#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <assert.h>
#include <errno.h>
#include <signal.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/dir.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <netdb.h>

#include "fd_list.h"
#include "bstrlib.h"
#include "server.h"
#include "receive.h"
#include "send_file_listing.h"
#include "send_metadata.h"
#include "send_slice.h"
#include "common.h"

int finish_var = 0, global_port=19500, server_num=0;
char** servers_list=NULL;

void finish(int i){
    finish_var = 1;
}

int verific(int *x)
{
  int z=0;
  z=scanf(" %i",x);
  while (z!=1)
    {
      while(scanf("%*[^\n]")!=0);
      printf ("\a\a\a\aHa ingresado mal el dato:\n");
      printf("\a\a\a\aIngrese correctamente lo requerido\n\t= ");
      z=scanf(" %i",x);
    }  
  
  return z;
}

void conf_menu(void){
    int finish=0, opt=0, i=0, del=-1;
    char *host_addr=NULL, *host_port=NULL;

    while(!finish){
        printf("****************************************************\n");
        printf("***************MENU DE CONFIGURACION****************\n");
        printf("****************************************************\n");
        printf("1. Puerto donde escuchar (actual: %i)\n", global_port);
        printf("2. Añadir un servidor a donde conectarse\n");
        printf("3. Listar servidores cargados\n");
        printf("4. Eliminar servidores cargados\n");
        printf("5. Ejecutar servidor\n");
        printf("6. Salir\n");
        printf("--> ");
        scanf("%i",&opt);
        while(opt<1 || opt>6){
            printf("\nIngrese la opcion correcta\n");
            printf("--> ");
            verific(&opt);
        }
        switch (opt){
            case 1:
            printf("\nIngrese el puerto (mayor que 1024 y menor que 65535):");
            scanf("%i",&global_port);
            while(global_port<1024 || global_port>65535){
                printf("\nIngrese la opcion correcta\n");
                printf("--> ");
                verific(&global_port);
            }
            break;

            case 2:
            printf("\nIngrese host a donde conectarse (host o ip):");
            host_addr=calloc(255, sizeof(char));
            scanf("%s", host_addr);
            printf("\nIngrese el puerto del host:");
            host_port=calloc(6, sizeof(char));
            scanf("%s", host_port);
            servers_list = realloc(servers_list, sizeof(char*)*(server_num+1));
            servers_list[server_num] = calloc((strlen(host_addr)+strlen\
                                       (host_port)+2), sizeof(char));
            strncpy(servers_list[server_num],host_addr,strlen(host_addr));
            strncat(servers_list[server_num],":",1);
            strncat(servers_list[server_num],host_port,strlen(host_port));
            server_num++;
            free(host_addr);
            host_addr = NULL;
            free(host_port);
            host_port=NULL;
            break;

            case 3:
            system("clear");
            printf("\nServidores cargados:\n");
            for(i=0;i<server_num;i++){
                printf("%s\n",servers_list[i]);
            }
            printf("\n");
            break;

            case 4:
            while(del!=0){

                system("clear");
                printf("\nServidores cargados:\n");
                for(i=0;i<server_num;i++){
                    printf("%i) %s\n",(i+1),servers_list[i]);
                }
                printf("\n");

                printf("Ingrese numero de servidor a eliminar (0 para salir): ");
                scanf("%i",&del);
                while(del<0 || del>server_num){
                    printf("\nIngrese la opcion correcta\n");
                    printf("--> ");
                    verific(&del);
                }
                if(del!=0){
                    free(servers_list[del-1]);
                    for(i=(del-1);i<server_num;i++){
                        servers_list[i] = servers_list[i+1];
                    }
                    servers_list[i]=NULL;
                    server_num--;

                }

            }
            del = -1;
            break;

            case 5:
            finish=1;
            break;

            case 6:
            for(i=0;i<server_num;i++){
                free(servers_list[i]);
                servers_list[i]=NULL;
            }
            free(servers_list);
            servers_list=NULL;
            system("clear");
            printf("Adiós\n");
            exit(1);
        }
    }


}


int setup_connection(void){
    /*Funcion utilizada para crear el socket y dejarlo escuchando en
    el puerto*/
    int fd_server = 0, yes=1;
    struct sockaddr_in server;

    /*Creamos el socket con protocolo de internet IPv4, de tipo stream
    (confiable)*/
    fd_server = socket(PF_INET, SOCK_STREAM, 0);
    if (fd_server == -1){
        perror("socket()");
        exit(1);
    }

    /*seteamos los valores de puerto y direccion del server utilizando
    funciones para convertir los ordenes de bytes, aumentando la portabilidad
    a otros sistemas*/

    server.sin_family = AF_INET;
    server.sin_port = htons(global_port);
    /*INADDR_ANY se usa para autodetectar la direccion del servidor*/
    server.sin_addr.s_addr = htonl(INADDR_ANY); 

    /*Seteamos el socket para permitir que el puerto sea reusado*/
    if (setsockopt(fd_server, SOL_SOCKET, SO_REUSEADDR,\
        &yes, sizeof(int)) == -1){
        perror("setsockopt");
        exit(1);
    }

    /*Bindeamos el socket con el puerto y direccion en el servidor*/
    if(bind(fd_server, (struct sockaddr*)&server,\
       sizeof(struct sockaddr)) == -1){
        perror("bind()");
        exit(1);
    }

    /*Empezamos a escuchar conexiones entrantes, vamos a atender un maximo de
    Q_CONEX conexiones*/
    if(listen(fd_server, Q_CONEX) == -1) {
        perror("listen()");
        exit(1);
    }

    printf("Servidor corriendo...\n");

    return fd_server;
}

int listen_and_accept_connection(int fd_server){
    int aux_size = 0, fd_client = 0;
    char *aux_inet = NULL;
    struct sockaddr_in client;


    assert(fd_server != 0);
    aux_size = sizeof(struct sockaddr);
    /*Aceptamos la conexion entrante*/
    fd_client = accept(fd_server, (struct sockaddr *)&client,\
                (socklen_t *)&aux_size);
    if (fd_client < 0) {
        perror("accept()");
        finish(1);
    }
    aux_inet = inet_ntoa(client.sin_addr);
    printf("Se obtuvo una conexion desde %s\n", aux_inet);
    return fd_client;

}


int main(int argc, char* argv[]){
    int fd_server=0, fd_client=0, i=0, j=0, h=0, s=0, maxDescriptor=0,\
        recvd=0, result=0, connection=0, sep=0, response=0;
    char *host_addr=NULL, *host_port=NULL;
    fd_set read_fds, write_fds;
    fd_list list = NULL;
    struct sockaddr_in dest_addr;
    struct hostent *host;
    struct timeval tv;


    signal(SIGPIPE,SIG_IGN);

    signal(SIGHUP, finish);
    signal(SIGINT, finish);
    signal(SIGTERM, finish);


    system("clear");
    printf("                 .-\"\"-.\n");
    printf("                /      \\\n");
    printf("               /     (0 \\______\n");
    printf("               |         \"_____)\n");
    printf("               \\        ,-----' \n");
    printf("                \\_    _/\n");
    printf("                 /    \\\n");
    printf("                /      \\\n");
    printf("               /        \\\n");
    printf("              /          |\n");
    printf("             /        :  |\n");
    printf("            /     ;   :  |\n");
    printf("   \\\\\\     /  _.-'    :  |\n");
    printf("    \\\\\\\\  / _'        :  |\n");
    printf("     \\\\\\\\/ ;         :   /\n");
    printf("      \\\\  ;         :   /\n");
    printf("       \\   `._`-'_.'  _/\n");
    printf("        \\     ''' _.-' \n");
    printf("         \\      / /\n");
    printf("          \\    / /  \n");
    printf("           \\  /)(_______\n");
    printf("            )(_________< \n");
    printf("           (__________< \n");
    printf("\n\n*******************************\n");
    printf("***** Bienvenido a Server *****\n");
    printf("**********  V 0.7 b  **********\n");
    printf("*******************************\n");
    servers_list = calloc(1, sizeof(char*));

    if (argc==1){
        printf("\nNo se utilizaron parametros, cargando menu...\n\n");
        conf_menu();
    }

    else{
        global_port = atol(argv[1]);
        for (s=2;s<argc;s++){
            servers_list = realloc(servers_list, sizeof(char*)*(server_num+1));
            servers_list[server_num] = calloc(261, sizeof(char));
            strncpy(servers_list[server_num], argv[s], strlen(argv[s]));
            server_num++;
        }
    }

    /*Creamos nuestra lista de file descriptors*/
    list = fd_list_create();

    for(s=0;s<server_num;s++){
        host_addr=calloc(255, sizeof(char));
        host_port=calloc(6, sizeof(char));

        sep=0;
        h=0;
        for(j=0;(unsigned int)j<strlen(servers_list[s]);j++){
            if(servers_list[s][j] == ':'){
                sep=1;
            }
            else{
                if(!sep){
                    host_addr[j] = servers_list[s][j];
                }
                else{
                    host_port[h] = servers_list[s][j];
                    h++;
                }
            }
        }
        result = socket(PF_INET, SOCK_STREAM, 0);
        if (result == -1) {
            printf("Error al crear el Socket");
            return -1;
        }
        dest_addr.sin_family = AF_INET;
        dest_addr.sin_port = htons(atol(host_port));
        host = gethostbyname(host_addr);
        dest_addr.sin_addr= (*( struct in_addr*)(host -> h_addr_list[0]));
    
        connection = connect(result, (struct sockaddr *)&dest_addr,\
                    sizeof(struct sockaddr));
        if (connection != 0) {
            printf("Error al intentar establecer la conexión\n");
            perror("connect");
            return -1;
        }
        if (result>maxDescriptor){
            maxDescriptor = result;
        }
        fd_list_add_server_fd(list, result);
        i = fd_list_server_search(list, result);
        fd_list_set_server_port(list, i, atol(host_port));
        fd_list_set_server_cmd(list, i, 5);
        fd_list_server_send(list, i);
        free(host_addr);
        host_addr=NULL;
        free(host_port);
        host_port=NULL;
    }

    for(i=0;i<server_num;i++){
        free(servers_list[i]);
        servers_list[i]=NULL;
    }
    free(servers_list);
    servers_list=NULL;

    /*Seteamos lo necesario para quedarnos escuchando en el puerto*/
    fd_server = setup_connection();

    /*cargamos el valor del fd_server como el descriptor mas grande*/
    if (fd_server>maxDescriptor){
        maxDescriptor = fd_server;
    }

    /*Bucle que mantenemos infinitamente para aceptar todas las conexiones
    entrantes*/

    while(!finish_var){
        /*eliminamos lo que hay en cada estructura*/
        FD_ZERO(&read_fds);
        FD_ZERO(&write_fds);
        FD_SET(fd_server, &read_fds);
        tv.tv_sec = 0;
        tv.tv_usec = 1000;
        /*Aqui nos fijamos que fd's nos enviaron pedido de quit o
        cerraron su conexion repentinamente, removiendolos de nuestra
        lista*/
        for (j = 0; j < (fd_list_size(list)); j++){
            if(fd_list_get_cmd(list, j) == 3){
                /*Si el comando fue el 3, entonces el cliente nos ha
                pedido salir, a lo que respondemos, cerramos y removemos
                de la lista*/
                send_msg(fd_list_get_fd(list, j), MSG0);
                close(fd_list_get_fd(list, j));
                fd_list_remove(list, j);
            }
        }

        /*dentro de este for metemos en la estructura read_fds los fd
        de los que esperamos el cliente nos envie cosas, y en la
        estructura write_fds metemos a los que nos faltan datos para enviar*/
        for (j = 0; j < (fd_list_size(list)); j++){
            if(fd_list_get_finish(list, j)){
                /*si ya terminamos de atender el comando anterior
                entonces vamos a quedarnos a la escucha de un comando
                nuevo*/
                FD_SET(fd_list_get_fd(list, j), &read_fds);
            }
            else{
                /*si no terminamos de atender, lo ponemos en la
                estructura de "para escribir"*/
                FD_SET(fd_list_get_fd(list, j), &write_fds);
            }
        }
        for (j = 0; j < (fd_list_server_size(list)); j++){
            if(fd_list_server_should_receive(list, j)){
                /*estamos esperando respuesta de servidor*/
                FD_SET(fd_list_get_server_fd(list, j), &read_fds);
            }
        }
        if (select(maxDescriptor + 1, &read_fds, &write_fds, NULL, &tv) == -1){
            perror("select()");
            printf("Cerrando Servidor...\n");
            close(fd_server);
            fd_list_destroy(list);
            list=NULL;
            exit(1);
        }

        /*Recorremos todos los file descriptors en busca de quienes
        esperan algo, o quieren enviar algo*/
        for (i = 0; i <= maxDescriptor; i++){
            /*si entramos en este if es porque llego algo de un cliente*/
            if (FD_ISSET(i, &read_fds)){
                /*Recibimos un intento de conexion*/
                if (i == fd_server){
                    /*tomamos la nueva conexion y la cargamos en nuestra
                    lista*/
                    fd_client = listen_and_accept_connection(fd_server);
                    if (fd_client > maxDescriptor){
                        maxDescriptor = fd_client;
                    }
                    fd_list_add_fd(list, fd_client);
                }
                /*Si no entro al if anterior, quiere decir que quizas
                algun cliente nos esta pidiendo algo, con lo cual
                usamos la siguiente funcion para ver si el fd se encuentra
                en nuestra lista, caso contrario obtendremos un -1*/
                else{
                    j = fd_list_search(list, i);
                    /*Si el fd se encuentra en la lista, entonces un cliente
                    nos ha hecho un pedido, y tenemos en la posicion J de la
                    estructura, toda la informacion necesaria*/
                    if (j != -1){
                        /*Atendemos la peticion del cliente*/
                        receive_info(list, j);
                    }
                    j = fd_list_server_search(list, i);
                    /*Si el fd se encuentra en la lista, entonces un servidor
                    nos esta enviando algo*/
                    if (j != -1){
                        recvd = fd_list_get_server_cmd(list, j);
                        if (recvd == 0){
                            /*nos responden el get_file_listing*/
                            if(load_files(list, j)){
                                if (fd_list_get_server_recv(list, j)){
                                    fd_list_set_server_cmd(list, j, 1);
                                    fd_list_server_stop_receiving(list, j);
                                    fd_list_server_send(list, j);
                                    fd_list_reset_server_recv_str(list, j);
                                    fd_list_set_server_recv(list, j, false);
                                }
                                else{
                                    #ifdef __DEBUG__
                                    printf("No termine de recibir el get_file_\
listing, manteniendo los datos para el proximo bucle\n");
                                    #endif
                                }
                            }
                            else{
                                printf("Error al recibir el file listing\n");
                            }
                        }

                        else if (recvd == 1){
                            /*nos responden el get_metadata*/
                            if(load_metadata(list, j)){
                                if (fd_list_get_server_recv(list, j)){
                                    /*terminamos de recibir y esta todo
                                    en orden, seteamos para enviar el
                                    get_slice*/
                                    fd_list_set_server_cmd(list, j, 2);
                                    fd_list_server_stop_receiving(list, j);
                                    fd_list_server_send(list, j);
                                    fd_list_reset_server_recv_str(list, j);
                                    fd_list_set_server_recv(list, j, false);
                                }
                                else{
                                    #ifdef __DEBUG__
                                    printf("No termine de recibir el get_\
metadata, manteniendo los datos para el proximo bucle\n");
                                    #endif
                                }
                            }
                            else{
                                printf("Error al recibir el metadata\n");
                            }
                        }

                        else if (recvd == 2){
                            /*nos responden el get_slice*/
                            if(get_file_from_server(list, j)){
                                if (fd_list_get_server_recv(list, j)){
                                    /*terminamos de recibir y esta todo
                                    en orden, seteamos para mandar el new_file*/
                                    fd_list_set_server_cmd(list, j, 4);
                                    fd_list_server_stop_receiving(list, j);
                                    fd_list_server_send(list, j);
                                    fd_list_reset_server_recv_str(list, j);
                                    fd_list_set_server_recv(list, j, false);
                                }
                            }
                            else{
                                #ifdef __DEBUG__
                                printf("Recibi una parte del archivo\n");
                                #endif
                            }
                        }

                        else if (recvd == 4){
                            /*nos responden el new_file*/
                            response = receive_response(list,j);
                            if(response==0){
                                /*respuesta ok*/
                                /*seteamos para no atender mas de aca*/
                                #ifdef __DEBUG__
                                printf("Me respondieron correctamente al \
new_file, seteo entonces para pedir el metadata de nuevo\n");
                                #endif
                                fd_list_set_server_cmd(list, j, 1);
                                fd_list_server_stop_receiving(list, j);
                                fd_list_server_send(list, j);
                            }
                            else{
                                /*Respuesta de error*/
                                printf("El servidor respondio el new_file con \
mensaje de error %i\n", response);
                            }
                        }
                        else if (recvd == 5){
                            /*nos responden el hello*/
                            response = receive_response(list,j);
                            if(response==0){
                                /*respuesta ok*/
                                /*Pedimos el file_listing*/
                                #ifdef __DEBUG__
                                printf("Me respondieron correctamente al \
hello, seteo entonces para enviar el get_file_listing\n");
                                #endif
                                fd_list_set_server_cmd(list, j, 0);
                                fd_list_server_stop_receiving(list, j);
                                fd_list_server_send(list, j);
                            }
                            else{
                                /*Respuesta de error*/
                                printf("El servidor respondio mensaje de error\
 %i\n", response);
                            }
                        }
                    }
                }
            }
            /*Si no entro en el if anterior, es probable que haya un cliente
            esperando nuestra respuesta*/
            else if (FD_ISSET(i, &write_fds)){
                /*Intentamos buscar si el fd que espera datos se encuentra en
                nuestra lista*/
                j = fd_list_search(list, i);
                if (j != -1){
                    /*obtenemos el comando pedido por el cliente*/
                    recvd = fd_list_get_cmd(list, j);
                    /*si recibimos hello y no es el primer comando que nos
                    envia, entonces mandamos error 102 y cerramos la conexion*/
                    if (recvd == 5 && !(fd_list_is_first_time(list, j))){
                        send_msg(fd_list_get_fd(list, j), MSG102);
                        close(fd_list_get_fd(list, j));
                        fd_list_remove(list, j);
                    }
                    /*si no, evaluamos cual fue el comando*/
                    else{
                        if (recvd == 0){
                            /*Enviamos el listado de archivos*/
                            send_file_listing(list, j);
                        }
                        else if (recvd == 1){
                            /*Enviamos la metadata del archivo pedido*/
                            send_metadata(list, j);
                        }
                        else if (recvd == 2){
                            /*Enviamos la porcion del archivo pedido*/
                            send_slice(list, j);
                        }
                        else if (recvd == 4){
                            /*Recibios el new_file, lo procesamos y 
                            respondemos*/
                            attend_new_file(list, j);
                        }
                        else if (recvd == 5){
                            /*Atendemos el hello*/
                            if(attend_hello(list, j)){
                                /*si el comando hello es correcto, entonces
                                nos conectamos con el servidor*/
                                result = connect_to_server(list,j);
                                if (result!=-1){
                                    if (result > maxDescriptor){
                                        maxDescriptor = result;
                                    }
                                }
                                else{
                                    /*Hubo un error en la conexion*/
                                }
                            }
                            else{
                                /*Si el hello es incorrecto, mandamos el
                                mensaje de error y desconectamos al cliente*/
                                send_msg(fd_list_get_fd(list, j), MSG102);
                                close(fd_list_get_fd(list, j));
                                fd_list_remove(list, j);
                            }
                        }
                    }
                }
            }
        }
        /*Afuera del bucle de los fd clientes*/
        for (i=0;i<fd_list_server_size(list);i++){
            if (fd_list_server_should_send(list,i)){
            /*Tengo que continuar con este servidor*/
                if (fd_list_get_server_cmd(list,i)==0){
                    /*Tengo que pedir la lista de archivos*/
                    if(get_files(list, i)){
                        fd_list_server_not_send(list, i);
                        fd_list_server_receive(list, i);
                    }
                    else{
                        printf("Error enviando get_file_listing\n");
                        /*hubo error*/
                    }
                }

                else if (fd_list_get_server_cmd(list,i)==1){
                    /*Tengo que pedir el metadata del primer archivo*/
                    if(get_metadata(list, i)){
                        /*Si entra aca, entonces el primer archivo de la lista
                        no lo tengo, entonces espero la respuesta del metadata*/
                        fd_list_server_not_send(list, i);
                        fd_list_server_receive(list, i);
                    }
                    else{
                        #ifdef __DEBUG__
                        printf("Sali del metadata: ya estoy bajando el archivo\
, ya lo tengo, o la lista esta vacia.\n");
                        #endif
                        /*si entro aca, es porque:
                        a) la lista de archivos esta vacia.
                        b) el primer archivo esta siendo descargado
                        c) el archivo ya lo tengo completo*/
                        fd_list_server_send(list, i);
                        fd_list_server_stop_receiving(list, i);
                        if (fd_list_get_server_filenames_count(list, i) == 0){
                            /*si entra a este if es porque no tiene archivos
                            que yo no tenga*/
                            fd_list_server_not_send(list, i);
                            fd_list_server_stop_receiving(list, i);
                        }

                    }
                }

                else if(fd_list_get_server_cmd(list,i)==2){
                    /*Tengo el metadata cargado, tengo que hacer el get_slice*/
                    if(get_slice(list, i)){
                        /*Si entra aca, es porque se envio get_slice para
                        el primer elemento de la lista*/
                        fd_list_server_not_send(list, i);
                        fd_list_server_receive(list, i);
                    }
                    else{
                        printf("Error enviando get_slice\n");
                        /*si entro aca, es porque hubo algun error, vuelvo a
                        pedir el get_file_listing*/
                        fd_list_set_server_cmd(list, j, 0);
                        fd_list_server_stop_receiving(list, j);
                        fd_list_server_send(list, j);
                        fd_list_reset_server_recv_str(list, j);
                        fd_list_set_server_recv(list, j, false);
                        }
                    }

                else if(fd_list_get_server_cmd(list,i)==4){
                    configure_new_file(list, i);
                }
                else if (fd_list_get_server_cmd(list,i)==5){
                    /*Tengo que enviar el hello con mi puerto*/
                    if(send_hello(list, i, global_port)){
                        /*hello enviado*/
                        fd_list_server_not_send(list, i);
                        fd_list_server_receive(list, i);
                    }
                    else{
                        printf("Error enviando hello\n");
                        /*hubo un error*/
                    }
                }
            }

            else if (!(fd_list_server_should_send(list,i))&&\
                     !(fd_list_server_should_receive(list, i))&&\
                     !(fd_list_server_get_new_file(list, i))){
                /*Si entro a este if es porque el servidor tiene su new_file
                en falso y no esta esperando enviar ni recibir nada, por lo
                tanto, envío el new_file*/
                if(send_msg(fd_list_get_server_fd(list, i),"new_file\r\n")==-1){
                    fd_list_server_remove(list, i);
                }
                else{
                    fd_list_server_not_send(list, i);
                    fd_list_server_receive(list, i);
                    fd_list_server_set_new_file(list, i);
                    fd_list_set_server_cmd(list, i, 4);
                }

            }
            else if (!(fd_list_server_should_send(list,i))&&\
                     !(fd_list_server_should_receive(list, i))&&\
                     fd_list_server_should_start_over(list, i)){
                fd_list_server_clear_start_over(list, i);
                fd_list_set_server_cmd(list, i, 0);
                fd_list_server_stop_receiving(list, i);
                fd_list_server_send(list, i);
                fd_list_reset_server_recv_str(list, i);
                fd_list_set_server_recv(list, i, false);
            }
        }

    }

    printf("Cerrando Servidor...\n");
    close(fd_server);
    fd_list_destroy(list);
    list=NULL;
    exit(1);
}
